﻿using System;
using System.Text;
using System.Collections;
using System.Collections.Generic;

using TypeProxy = System.Collections.Generic.IDictionary<string, object>;

namespace Sencha.Serialization
{
	using Serializers;
	using Contracts;

	sealed class AdHocSerializer : ISerializationContext, IJsonSerializer
	{
		private DataContract m_contract;
		private string[] m_dateFormats;
		private Features m_features;
		private ulong m_objectCount;
		private Stack m_hierarchy;
		private IFormatProvider m_format;
		private Encoding m_encoding;

		public DataContract DataContract { get { return m_contract; } internal set { m_contract = value; } }
		public Features Features { get { return m_features; } set { m_features = value; } }
		public IFormatProvider Format { get { return m_format; } internal set { m_format = value; } }
		public Encoding Encoding { get { return m_encoding; } internal set { m_encoding = value; } }
		public string[] DateTimeFormats { get { return m_dateFormats; } internal set { m_dateFormats = value; } }
		public Stack Hierarchy { get { return m_hierarchy; } }
		public ulong MaxItemsInObjectGraph { get; set; }

		public AdHocSerializer()
		{
			m_hierarchy = new Stack();
			m_contract = DataContractCache.AdHocContract;
			m_features = Serialization.Features.ArrayInterchange | Serialization.Features.StringInterchange | Serialization.Features.SkipUnknownMember | Serialization.Features.SkipUnaccessibleMembers | Serialization.Features.SerializeByteArrayAsBase64;
			m_dateFormats = new string[Json.DateTimeFormats.Length + 1];
			m_dateFormats[0] = Json.DateTimeFormats[0];
			Json.DateTimeFormats.CopyTo(m_dateFormats, 1);
			m_encoding = Json.DefaultEncoding;
			m_format = Json.DefaultFormatProvider;

			this.MaxItemsInObjectGraph = int.MaxValue;
		}

		public void WriteValue<T>(IJsonWriter writer, T value, TypeDescription serializableType)
		{
			if (writer == null)
				throw new ArgumentNullException("writer");
			System.Diagnostics.Contracts.Contract.EndContractBlock();

			if (writer.Context == this)
			{
				this.m_objectCount++;
				if (this.m_objectCount > this.MaxItemsInObjectGraph)
					throw new Exceptions.SerializationGraphIsTooBig(this.MaxItemsInObjectGraph);
			}

			var nominalType = typeof(T);
			var actualType = nominalType.IsValueType || value == null ? nominalType : value.GetType();

			if (serializableType == null)
				serializableType = this.DataContract.GetKnownType(nominalType);
			serializableType = this.LiftTypeForSerialization(actualType, serializableType);

#if DEBUG
			if (!typeof(T).IsAssignableFrom(serializableType.GetClrType()))
				throw new  ArgumentException(string.Format( "Serialization type missmatch. Can't cast serialized type '{0}' to value type '{1}'", serializableType.TypeFullName, typeof(T).FullName), "serializableType");
#endif

			if (nominalType.IsValueType)
			{
				var serializer = default(TypeSerializer<T>);

				if (serializableType.IsJsonSerializable && (m_features & Serialization.Features.IgnoreCustomSerialization) == 0)
					serializer = CustomSerializer<T>.Instance;
				else
					serializer = (TypeSerializer<T>)this.DataContract.GetSerializerOfType(serializableType);

				serializer.Serialize(writer, value);
			}
			else
			{
				var serializer = default(ITypeSerializer);

				if (nominalType == typeof(System.IO.Stream))
					serializer = StreamSerializer.Instance;
				else if (nominalType == typeof(Byte[]) && (m_features & Serialization.Features.SerializeByteArrayAsBase64) != 0)
					serializer = Base64Serializer.Instance;
				else if (serializableType.IsJsonSerializable && (m_features & Serialization.Features.IgnoreCustomSerialization) == 0)
					serializer = CustomSerializer<T>.Instance;
				else
					serializer = this.DataContract.GetSerializerOfType(serializableType);

				serializer.Serialize(writer, value);
			}
		}
		public T ReadValue<T>(IJsonReader reader, TypeDescription serializableType)
		{
			if (reader == null)
				throw new ArgumentNullException("reader");
			System.Diagnostics.Contracts.Contract.EndContractBlock();

			reader.ThrowIfEndOfStream(JsonToken.BeginObject);

			if (reader.Context == this)
			{
				this.m_objectCount++;
				if (this.m_objectCount > this.MaxItemsInObjectGraph)
					throw new Exceptions.SerializationGraphIsTooBig(this.MaxItemsInObjectGraph);
			}

			var nominalType = typeof(T);

			if (serializableType == null)
				serializableType = this.DataContract.GetKnownType(nominalType);

			serializableType = this.LiftTypeForDeserialization(reader.Token, reader.RawValue, reader.Value.RawType, serializableType);

#if DEBUG
			if (!typeof(T).IsAssignableFrom(serializableType.GetClrType()))
				throw new  ArgumentException(string.Format( "Serialization type missmatch. Can't cast deserialized type '{0}' to resulting type '{1}'", serializableType.TypeFullName, typeof(T).FullName), "serializableType");
#endif

			// String intercharge
			if (reader.Token != JsonToken.StringLiteral && nominalType == typeof(string) && (m_features & Serialization.Features.StringInterchange) != 0 )
			{
				if (reader.Token == JsonToken.Null)
					return default(T);

				var jsonString = Serializers.JsonStringSerializer.Instance.Deserialize(reader);
				return (T)(object)Convert.ToString(jsonString);
			}


			if (nominalType.IsValueType)
			{
				var serializer = default(TypeSerializer<T>);

				if (serializableType.IsJsonSerializable && (m_features & Serialization.Features.IgnoreCustomSerialization) == 0)
					serializer = CustomSerializer<T>.Instance;
				else
					serializer = (TypeSerializer<T>)this.DataContract.GetSerializerOfType(serializableType);

				return serializer.Deserialize(reader);
			}
			else
			{
				var serializer = default(ITypeSerializer);

				if (nominalType == typeof(System.IO.Stream))
					serializer = StreamSerializer.Instance;
				else if (nominalType == typeof(Byte[]) && (m_features & Serialization.Features.SerializeByteArrayAsBase64) != 0)
					serializer = Base64Serializer.Instance;
				else if (serializableType.IsJsonSerializable && (m_features & Serialization.Features.IgnoreCustomSerialization) == 0)
					serializer = CustomSerializer<T>.Instance;
				else
					serializer = this.DataContract.GetSerializerOfType(serializableType);

				return (T)serializer.Deserialize(reader);
			}
		}
		object IJsonSerializer.ReadObject(System.IO.TextReader reader)
		{
			throw new NotSupportedException();
		}
		void IJsonSerializer.WriteObject(System.IO.TextWriter output, object graph)
		{
			throw new NotSupportedException();
		}

		private TypeDescription LiftTypeForDeserialization(JsonToken token, object value, Type valueType, TypeDescription expectedType)
		{
			if (expectedType == null)
				throw new ArgumentNullException("expectedType");
			System.Diagnostics.Contracts.Contract.EndContractBlock();

			if (!expectedType.IsObjectType)
				return expectedType; // don"t lift concrete type

			if (token == JsonToken.Null)
				return this.DataContract.GetKnownType(typeof(object));

			var knownTypeInfo = (TypeDescription)null;
			if (valueType != null && m_contract.TryGetKnownType(valueType, out knownTypeInfo))
				return knownTypeInfo;
			else if (token == JsonToken.BeginArray)
				knownTypeInfo = this.DataContract.AddKnownType(typeof(object[]));
			else if (token == JsonToken.BeginObject)
				knownTypeInfo = this.DataContract.AddKnownType(Json.DefaultDictionaryType);
			else if (valueType != null)
				knownTypeInfo = this.DataContract.AddKnownType(valueType);
			else
				throw new Exceptions.JsonSerializationException("Unable to pick type for deserialization");

			return knownTypeInfo;
		}
		private TypeDescription LiftTypeForSerialization(Type actualType, TypeDescription expectedType)
		{
			if (expectedType == null)
				throw new ArgumentNullException("expectedType");
			System.Diagnostics.Contracts.Contract.EndContractBlock();

			if (expectedType.GetClrType() == actualType)
				return expectedType;

			TypeDescription foundType = (TypeDescription)null;
			Type type;
			for (type = actualType; type != null; type = type.BaseType)
			{
				if (this.m_contract.TryGetKnownType(type, out foundType))
				{
					if (!foundType.IsDescendantOf(expectedType))
						foundType = (TypeDescription)null;
					else
						break;
				}
			}
			if (foundType != null && foundType.IsDescendantOf(expectedType) && !foundType.IsObjectType)
				return foundType;

			TypeDescription typeInfo = (TypeDescription)null;
			if (expectedType.IsInterface || expectedType.IsObjectType)
			{
				var actualInterfaces = actualType.GetInterfaces();
				foreach (Type publicInterface in actualInterfaces)
				{
					TypeDescription foundInterface = (TypeDescription)null;
					if (this.m_contract.TryGetKnownType(publicInterface, out foundInterface))
					{
						var isExpectedInterface = foundInterface.IsDescendantOf(expectedType);
						var isNotDescendantOfCurrentTypeInfo = typeInfo == null || foundInterface.IsDescendantOf(typeInfo);
						var isMoreSpecificInterface = (foundInterface.IsEnumerable || foundInterface.IsDictionary ? 0 : 1) >= (typeInfo == null || typeInfo.IsEnumerable || typeInfo.IsDictionary ? 0 : 1);

						if (isExpectedInterface && isNotDescendantOfCurrentTypeInfo && isMoreSpecificInterface)
							typeInfo = foundInterface;
					}
				}
			}
			if (typeInfo != null && typeInfo.IsDescendantOf(expectedType) && !typeInfo.IsObjectType)
				return typeInfo;
			else
				return expectedType;
		}

		internal void Reset()
		{
			m_objectCount = 0;
			m_hierarchy.Clear();
		}
	}
}
